package pl.szpadel.android.gadu;

import java.io.StringReader;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;

import pl.szpadel.android.gadu.packets.AddNotify;
import pl.szpadel.android.gadu.packets.BasePacket;
import pl.szpadel.android.gadu.packets.Notify;
import pl.szpadel.android.gadu.packets.NotifyReply80;
import pl.szpadel.android.gadu.packets.ReceivedPacket;
import pl.szpadel.android.gadu.packets.UserlistReply80;
import pl.szpadel.android.gadu.packets.UserlistRequest;
import pl.szpadel.android.gadu.packets.ZeroLenghtSendPacket;
import android.content.Context;
import android.database.DataSetObserver;

/** Singleton containing all contact data */
public class ContactInfoManager extends Object implements IAppComponent {

	private static final String TAG = "ContactInfoManager";
	
	///////////////////////
	// Contacts db
	
	/// Actual database
	private HashMap<Long, ContactInfo> mById = new HashMap<Long, ContactInfo>();
	
	/// UIn of logged in user for which contacts are showed
	private long mCurrentUin = 0;
	
	/// buffer from incoming userlist messages
	private ArrayList<ByteBuffer> mReceivedBuffers = new ArrayList<ByteBuffer>();
	
	/// Contacts to which we subscribed in the server
	private HashSet<Long> mSubscribedContacts = new HashSet<Long>();
	
	/// Flag - contact were loaded from DB during this session
	private boolean mContactsLoaded = false;
	
	ContactDatabaseOpenHelper mDBHelper;

	/// Adds user to the info manager, or updates if already exists
	public synchronized void addContact(ContactInfo contact) {
		if (mById.containsKey(contact.id)) {
			updateInDB(contact);
		} else {
			addToDB(contact);
		}
		mById.put(contact.id, contact);
		
		// if connected and not subscribed to contact state, do it.
		if (App.getInstance().getConnectionState().getState() == ConnectionState.CONNECTED) {
			if (! mSubscribedContacts.contains(contact.id)) {
				subscribeToSingleContact(contact.id);
			}
		}
		
		notifyObservers();
	}
	
	/// Returns contact by uin
	public synchronized ContactInfo getByUin(long uin) {
		return mById.get(uin);
	}
	
	/// Request download from server
	public void importContactListFromServer() {
		AGLog.i(TAG, "importContactListFromServer");
		// create req message
		App.getInstance().getCommunicationService().sendPacket(new UserlistRequest());
		// prepare to receive packets
		mReceivedBuffers.clear();
	}
	
	/// Handles incoming userlist packet
	private void handleUserlistPacket(UserlistReply80 packet) {
		AGLog.i(TAG, "onUserlistPacket");
		
		mReceivedBuffers.add(packet.data);
		if (packet.last) {
			// count total data
			int compressedDataLen = 0;
			for (ByteBuffer b: mReceivedBuffers) {
				compressedDataLen += b.limit();
			}
			AGLog.i(TAG, "total compressed data size:" + compressedDataLen);
			// allocate buffer to hold them all
			ByteBuffer compressedData = ByteBuffer.allocate(compressedDataLen);
			for (ByteBuffer b: mReceivedBuffers) {
				b.rewind();
				compressedData.put(b);
			}
			// allocate data for decompressed
			byte[] decompressedData = new byte[compressedDataLen*100]; // i don't like this
			// decompress
			Inflater inflater = new Inflater();
			inflater.setInput(compressedData.array());
			
			try {
				int decompressedDataLen = inflater.inflate(decompressedData, 0, decompressedData.length);
				AGLog.i(TAG, "decompressed data size:"+ decompressedDataLen);
				String asString = new String(decompressedData, 0, decompressedDataLen);
				//AGLog.i(TAG, "Contact list:\n" + asString);
				parseServerContactList(asString);
			} catch (DataFormatException e) {
				AGLog.e(TAG, "Error decompressing received list:"+ e.getMessage());
			}
			
		}
	}
	
	/// Updates 
	
	/// Returns shallow copy all contacts
	public synchronized Collection<ContactInfo> getContacts() {
		return mById.values();
	}
	
	/// Private constructor
	public ContactInfoManager(Context ctx) {
		mDBHelper = new ContactDatabaseOpenHelper(ctx);
	}

	/////////////////////////////////////
	// Change notifications

	/// Set of observes. We need no more than one instance of every observer
	private HashSet<DataSetObserver> mObservers = new HashSet<DataSetObserver>();
	
	/// Flag - bulk change in progress
	boolean mBulkChange = false;
	
	/// changes during bulk change operation
	int mBulkChangeChanges = 0;
	
	////////////////////////////////////////////////////////
	// interface
	
	/// starts bulk change - all changes will be applied only after endChanges() is called
	public void beginChanges() {
		assert(mBulkChange == false);
		
		mBulkChange = true;
		mBulkChangeChanges = 0;
	}
	
	/// Ends bulk change. Notifies observes if something happened since last beginChanges()
	public void endChanges() {
		assert(mBulkChange == true);
		mBulkChange = false;
		
		if (mBulkChangeChanges > 0 ) {
			for (DataSetObserver observer : mObservers ) {
				observer.onChanged();
			}
			mBulkChangeChanges = 0;
		}
	}
	
	public void registerDataSetObserver(DataSetObserver observer) {
		mObservers.add(observer);
	}

	public void unregisterDataSetObserver(DataSetObserver observer) {
		mObservers.remove(observer);
		
	}

	// Notifies observers of change
	private void notifyObservers() {
		if (mBulkChange == true) {
			AGLog.d(TAG, "notifyObservers: doing nothing, bulk change");
			mBulkChangeChanges++;
		} else {
			AGLog.d(TAG, "notifyObservers: notyfying");
			for (DataSetObserver observer : mObservers ) {
				observer.onChanged();
			}
		}
	}
	
	@Override
	public void onPacket(ReceivedPacket packet) {
		
    	switch(packet.getType()) {
    	case BasePacket.TYPE_NOTIFY_REPLY80:
    	case BasePacket.TYPE_STATUS80:
    		updateStatuses((NotifyReply80) packet);
    		break;
    	case BasePacket.TYPE_USERLIST_REPLY80:
    		handleUserlistPacket((UserlistReply80)packet);
    		break;
    	default:
    		; // do nothing
    	}
		
	}

	@Override
	public void onConnectionStateChanged(ConnectionState state) {
		 // just connected?
		if (state.getState() == ConnectionState.CONNECTED) {
			// logged in user changed?
			if (mCurrentUin != App.getInstance().getConfig().getUin()) {
				// clear current user list
				mById.clear();
				mContactsLoaded = false;
				mCurrentUin = App.getInstance().getConfig().getUin();
				notifyObservers();
			}
			// load contacts from DB if not yet loaded
			if (!mContactsLoaded) {
				loadContactsFromDB();
				mContactsLoaded = true;
			}
			// subscribe to contacts
			mSubscribedContacts.clear();
			subscribeToAllContacts();
		}
		// disconnected?
		else {
			// set all statuses to 'offline'
			AGLog.i(TAG, "Connection lost, setting al statuses to offline");
			setAllContactsOffline();
		}
		
	}
	
	// Helper - translates uin to name
	public String uinToName(long uin) {
		if (mById.containsKey(uin)){
			ContactInfo ci = mById.get(uin);
			if (ci.displayName != "") {
				return ci.displayName;
			}
		}
		
		return Long.toString(uin);
	}
	
	/// Loads contact from DB
	public void loadContacts() {
		if (!mContactsLoaded) {
			loadContactsFromDB();
			mContactsLoaded = true;
		}
	}

	/// deletes contact
	public void deleteContact(long uin) {
		if (mById.containsKey(uin)){
			mById.remove(uin);
			mDBHelper.deleteContact(uin);
			notifyObservers();
		}
	}

	/////////////////////////////////////////////////////////
	// Implementation
	
	private void subscribeToAllContacts() {
		// subscribes to all contacts
		// send contact list
		AGLog.i(TAG, "Subscrining to the entire contact list");
		if (mById.isEmpty()){
			AGLog.d(TAG, "Sending empty contact list");
			ZeroLenghtSendPacket cl = new ZeroLenghtSendPacket(BasePacket.TYPE_LIST_EMPTY);
			App.getInstance().getCommunicationService().sendPacket(cl);
		}
		else {
			// TODO handle contact lists above 400
			assert(mById.size() <= 400);
			AGLog.d(TAG, "Sending list notification");
			Notify notify = new Notify(mById.values(), true);
			App.getInstance().getCommunicationService().sendPacket(notify);
			
			mSubscribedContacts.addAll(mById.keySet());
		}		
	}
	
	private void subscribeToSingleContact(long id) {
		AGLog.i(TAG, "Subscribint to single packet: " + id);
		AddNotify packet = new AddNotify((int)id);
		App.getInstance().getCommunicationService().sendPacket(packet);
	}

	
	/// contact list parser handler
	private class ContactListHandler extends DefaultHandler {
		
		/// Last element content
		private String mElementContent = new String();
		ContactInfo mContact = null; // currently parsed contact
		
		/// On element started
		public void startElement (String uri, String localName, String qName, Attributes attributes) {
			if (localName == "Contact") {
				mContact = new ContactInfo("", -1);
			}
		}
		/// On element content
		public void characters (char[] ch, int start, int length) {
			mElementContent = new String(ch, start, length);
		}
		/// On element ended
		public void endElement (String uri, String localName, String qName) {
			if (mContact != null) {
				try {
					AGLog.d(TAG, "parsing contact param: "+ localName + " = " + mElementContent);
					
					if (localName == "GGNumber") mContact.id = Long.parseLong(mElementContent);
					else if (localName == "ShowName" )mContact.displayName = mElementContent;
					else if (localName == "MobilePhone" )mContact.mobilePhone = mElementContent;
					else if (localName == "HomePhone" )mContact.homePhone = mElementContent;
					else if (localName == "Email" )mContact.email = mElementContent;
					else if (localName == "FirstName" )mContact.firstName = mElementContent;
					else if (localName == "LastName" )mContact.lastName = mElementContent;
					else if (localName == "WwwAddress" )mContact.wwwAdrress = mElementContent;
					else if (localName == "Gender" )mContact.gender = mElementContent;
					else if (localName == "Birth" )mContact.birth = mElementContent;
					else if (localName == "City" )mContact.city = mElementContent;
					else if (localName == "Contact") {
						// validate
						if (mContact.id < 0){
							AGLog.e(TAG, "got from XML conatct with no GGNumber: " + mContact);
						} else {
							addContact(mContact);
						}
						mContact = null;
					}
				} catch (Exception e) {
					AGLog.e(TAG, "Error parsing contact: " + mContact);
					mContact = null; // will skip it and wait for the next one
				}
			}
		}
	}
	
	/// Parses contact list as send by a server
	// format description: http://dev.gg.pl/api/pages/formaty_plikow.html
	private void parseServerContactList(String xml) {
		AGLog.i(TAG, "parsing xml");
		
		try {
			SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
			InputSource is = new InputSource(new StringReader(xml));
			
			beginChanges();
			parser.parse(is, new ContactListHandler());
			endChanges();
			
			// set user list to top
			if (!mById.isEmpty()) {
				ContactListActivity activity = App.getInstance().getContactListActivity();
				if (activity != null) {
					activity.setSelection(0);
				}
			}
			
		} catch (Exception e) {
			AGLog.e(TAG, "Error parsing contact list:" + e.getMessage());
		}
		
	}
	
    /// Updates statuses
    private void updateStatuses(NotifyReply80 statuses) {
    	for (NotifyReply80.StatusInfo status : statuses.statuses){
    		AGLog.d(TAG, "Updating status: " + status);
    		
    		ContactInfo ci = mById.get(status.uin);
    		if (ci != null) {
    			AGLog.i(TAG, "Updating status for user " + status.uin + " to " + status.status);
    			ci.status = status.status;
    		}
    	}
    	notifyObservers();
    }
    
    /// Sets all contact statuses to offline
	private void setAllContactsOffline() {
		// change all statuses
		for(ContactInfo ci : mById.values()) {
			ci.status = new Status(Status.GG_STATUS_NOT_AVAIL, ci.status.getDescription());
		}
		// tell everyone
		notifyObservers();
	}

	@Override
	public void onStatusChanged(Status status) {
		// nothing
		
	}

	@Override
	public void onConfigChanged() {
		// TODO Auto-generated method stub
		
	}
	
	/// Adds single contacts to DB
	private void addToDB(ContactInfo contact) {
		mDBHelper.insert(contact);
	}
	
	/// updates single contact in db
	private void updateInDB(ContactInfo contact) {
		mDBHelper.update(contact);
	}
	
	/// load all contacts from DB
	private void loadContactsFromDB() {
		Collection<ContactInfo> contacts = mDBHelper.getAllContacts();
		if (!contacts.isEmpty()) {
			for (ContactInfo info : contacts) {
				mById.put(info.id, info);
			}
			notifyObservers();
		}
	}
	

}
